/*  
 * i-OSGi - Tunable Bundle Isolation for OSGi
 * Copyright (C) 2011  Sven Schulz
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.iosgi.impl;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Semaphore;
import java.util.jar.Attributes;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.felix.ipojo.annotations.Component;
import org.apache.felix.ipojo.annotations.Provides;
import org.apache.felix.ipojo.annotations.Requires;
import org.apache.felix.ipojo.annotations.ServiceProperty;
import org.apache.felix.service.command.CommandProcessor;
import org.iosgi.Constants;
import org.iosgi.IsolatedFramework;
import org.iosgi.IsolationAdmin;
import org.iosgi.IsolationConstraint;
import org.iosgi.IsolationConstraintRegistry;
import org.iosgi.IsolationDirective;
import org.iosgi.IsolationEnvironment;
import org.iosgi.IsolationEnvironmentFactory;
import org.iosgi.engine.IsolationEngine;
import org.iosgi.engine.Operation;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Filter;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceListener;
import org.osgi.framework.ServiceReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author Sven Schulz
 */
@Component(immediate = true)
@Provides(specifications = { IsolationAdmin.class })
public class IsolationAdminImpl implements IsolationAdmin {

	private static final Logger LOGGER = LoggerFactory
			.getLogger(IsolationAdminImpl.class);
	private static final Pattern DIRECTIVE = Pattern
			.compile("level:=([^;]*);([^,]*)");

	private static Dictionary<String, String> toDictionary(Attributes attrs) {
		Dictionary<String, String> d = new Hashtable<String, String>();
		for (Object k : attrs.keySet()) {
			Attributes.Name name = (Attributes.Name) k;
			d.put(name.toString(), attrs.getValue(name).toString());
		}
		return d;
	}

	@ServiceProperty(name = CommandProcessor.COMMAND_SCOPE)
	public String commandScope = "iosgi";

	@ServiceProperty(name = CommandProcessor.COMMAND_FUNCTION)
	public String[] commandFunction = new String[] { "environments",
			"environment", "spawn", "install", "uninstall", "bundle" };

	@Requires(optional = true)
	private IsolationEnvironment[] environments;

	@Requires(optional = true)
	private IsolatedFramework[] frameworks;

	@Requires(optional = true)
	private IsolationEnvironmentFactory[] factories;

	@Requires
	private IsolationConstraintRegistry registry;

	@Requires
	private IsolationEngine engine;

	@Requires(optional = true)
	private Bundle[] bundles;

	private Map<String, List<IsolationDirective>> directives;
	private Map<String, Dictionary<String, String>> manifestEntries;
	private final BundleContext context;

	IsolationAdminImpl(BundleContext context) {
		this.context = context;
		directives = new HashMap<String, List<IsolationDirective>>();
		manifestEntries = new HashMap<String, Dictionary<String, String>>();
	}

	public void environments() {
		for (IsolationEnvironment ie : environments) {
			System.out.println(ie.getId());
		}
	}

	public IsolationEnvironment environment(String id) {
		return getEnvironment(URI.create(id));
	}

	@Override
	public IsolationEnvironment getEnvironment(URI id) {
		for (IsolationEnvironment ie : environments) {
			if (ie.getId().equals(id)) {
				return ie;
			}
		}
		return null;
	}

	@Override
	public IsolatedFramework getIsolatedFramework(URI environmentId) {
		for (IsolatedFramework f : frameworks) {
			if (f.getId().equals(environmentId)) {
				return f;
			}
		}
		return null;
	}

	@Override
	public URI spawn(URI parent) throws Exception {
		String pssp = parent.getSchemeSpecificPart();
		Map<String, Object> props = new HashMap<String, Object>();
		for (IsolationEnvironmentFactory f : factories) {
			if (f.getId().getSchemeSpecificPart().equals(pssp)) {
				return f.newIsolationEnvironment(props);
			}
		}
		throw new IOException("no matching factory found (" + parent + ")");
	}

	@Override
	public void destroy(final URI id) throws Exception {
		IsolationEnvironment env = getEnvironment(id);
		final Semaphore s = new Semaphore(0);
		ServiceListener l = new ServiceListener() {
			@Override
			public void serviceChanged(ServiceEvent event) {
				if (event.getType() != ServiceEvent.UNREGISTERING) {
					return;
				}
				ServiceReference ref = event.getServiceReference();
				String[] objClasses = (String[]) ref.getProperty("objectClass");
				boolean contains = Arrays.binarySearch(objClasses,
						IsolationEnvironment.class.getName()) >= 0;
				if (contains && ref.getProperty("environment.id").equals(id)) {
					s.release();
				}
			}
		};
		context.addServiceListener(l);
		try {
			env.destroy();
			s.acquire();
		} finally {
			context.removeServiceListener(l);
		}
	}

	@Override
	public Bundle getBundle(String location) {
		for (Bundle b : bundles) {
			String loc = b.getLocation();
			if (loc.equals(location)) {
				return b;
			}
		}
		return null;
	}

	public Bundle bundle(String location) {
		return this.getBundle(location);
	}

	@Override
	public void install(final String location) throws IOException {
		URL lurl = new URL(location);
		InputStream is = lurl.openStream();
		JarInputStream jis = new JarInputStream(is);
		Manifest mf = jis.getManifest();
		if (mf == null) {
			throw new IOException("missing manifest");
		}
		Dictionary<String, String> headers = toDictionary(mf
				.getMainAttributes());
		manifestEntries.put(location, headers);
		String s = headers.get(Constants.BUNDLE_ISOLATION);
		if (s == null) {
			LOGGER.debug("no isolation constraints specified in manifest");
		} else {
			List<IsolationDirective> directives = this.getDirectives(location);
			Matcher m = DIRECTIVE.matcher(s);
			while (m.find()) {
				int level = Integer.parseInt(m.group(1));
				Filter filter = null;
				try {
					filter = FrameworkUtil.createFilter(m.group(2));
				} catch (InvalidSyntaxException ise) {
					throw new IOException(
							"invalid filter in isolation directive: "
									+ m.group(), ise);
				}
				IsolationDirective d = new IsolationDirective(level, filter);
				directives.add(d);
				LOGGER.debug("directive {} added", d);
			}
			for (IsolationDirective d : directives) {
				Filter f = d.getFilter();
				int level = d.getLevel();
				for (Map.Entry<String, Dictionary<String, String>> e : manifestEntries
						.entrySet()) {
					String otherLocation = e.getKey();
					if (!otherLocation.equals(location)
							&& f.match(e.getValue())) {
						IsolationConstraint c = new IsolationConstraint(
								location, otherLocation, level);
						registry.add(c);
					}
				}
			}
		}
		Dictionary<String, String> props = manifestEntries.get(location);
		for (Map.Entry<String, List<IsolationDirective>> e : directives
				.entrySet()) {
			String otherLocation = e.getKey();
			if (otherLocation.equals(location))
				continue;
			for (IsolationDirective d : e.getValue()) {
				Filter f = d.getFilter();
				if (f.match(props)) {
					int level = d.getLevel();
					IsolationConstraint c = new IsolationConstraint(
							otherLocation, location, level);
					registry.add(c);
				}
			}
		}
		List<Operation> ops = null;
		try {
			ops = engine.install(location);
		} catch (Exception e) {
			/* TODO Remove all isolation constraints for bundle. */
			throw new IOException(e);
		}
		execute(ops);
	}

	@Override
	public void uninstall(String location) throws IOException {
		for (IsolationConstraint c : registry.getConstraints(location)) {
			registry.remove(c);
		}
		List<Operation> ops = null;
		try {
			ops = engine.uninstall(location);
		} catch (Exception e) {
			/* TODO Remove all isolation constraints for bundle. */
			throw new IOException(e);
		}
		execute(ops);
	}

	private void execute(List<Operation> ops) throws IOException {
		LOGGER.debug("performing {} operations ({})", ops.size(), ops);
		for (Operation op : ops) {
			try {
				LOGGER.debug("performing {}", op);
				op.perform(this);
			} catch (Exception e) {
				/* TODO Perform rollback on engine. */
				throw new IOException("operation (" + op + ") failed", e);
			}
		}
	}

	private List<IsolationDirective> getDirectives(final String location) {
		List<IsolationDirective> d = directives.get(location);
		if (d == null) {
			directives.put(location, d = new ArrayList<IsolationDirective>());
		}
		return d;
	}

}